//
//  Dive.m
//  DiveCalcsApp
//
//  Created by Donald MacKay on 11-04-07.
//  Copyright 2011 __MyCompanyName__. All rights reserved.
//

#import "Dive.h"
#import "Constants.h"
#import <math.h>


@implementation Dive

// Synthesize the methods
@synthesize maxDepth, diveTime, gasSwitchDepth, consumptionRate, ascRateTimeInterval, minGasAscentTime;
@synthesize msText, measurementSystem, diveProfile, ascRateChangePercent, ascRateChangeDepth;

// Initializers
-(id) initWithDepth: (NSInteger) d time: (NSInteger) t switchDepth: (NSInteger) gs SAC: (float) sac andIsMetric:(BOOL) metric
{
    self = [super init];
 
//    NSString *path = [[NSBundle mainBundle] pathForResource:@"metric" ofType:@"plist"];
//    NSString *path = @"metric.plist";
    
    if (self) {
        maxDepth = d;
        diveTime = t;
        gasSwitchDepth = gs;
        consumptionRate = [NSNumber numberWithFloat:sac];
        measurementSystem = metric;
        diveProfile = [[NSMutableArray alloc] init];
        ascRateChangePercent = [[NSMutableArray alloc] init];
        ascRateChangeDepth = [[NSMutableArray alloc] init];
        ascRateTimeInterval = [[NSMutableArray alloc] init];

#ifdef DEBUG // Set up for test as an app does not have a resource bundle
        
        // The keys are the same for both imperial or metric
        NSArray *keys = [NSArray arrayWithObjects:@"DeepAscentRate", @"IntermediateAscentRate", @"ShallowAscentRate",
                         @"IntervalRoundingAmount", @"OneAtmosphere", @"RDAscentTimePerUnit",
                         @"RDDepthSetPoint", @"StopIncrement", @"Unit", @"VolumeUnit",nil]; 
        
        if (metric) {
            NSArray *objects = [NSArray arrayWithObjects:@"9", @"3", @"1.5", @"1.5",
                                @"10", @"0.5", @"45",
                                @"3", @"meters", @"Liters", nil];

            msText = [NSDictionary dictionaryWithObjects:objects forKeys:keys];

        } else {
            NSArray *objects = [NSArray arrayWithObjects:@"30", @"10", @"5", @"5",
                                @"33", @"1.67", @"150",
                                @"10", @"feet", @"cuft", nil];

            msText = [NSDictionary dictionaryWithObjects:objects forKeys:keys];
        }
#else // Set up for using a project resource bundle when compiling for iPad or iPhone project
    
        // Select and load the correct resource bundle
        if (metric) {
            path=[[NSBundle mainBundle] pathForResource:@"metric" ofType:@"plist"];
            
        } else {
            path=[[NSBundle mainBundle] pathForResource:@"imperial" ofType:@"plist"];
        }
        
        msText = [NSDictionary initWithContentsOfFile:path];
#endif
        // Set up the ascent rate change thresh holds. FBOX()is a macro defined
        // in constants.h. If eventually it is necessary to incorporate a third asc rate
        // chage just add FBOX(kThirdAscRateChange) to the end of the array
        
        if (maxDepth <= (measurementSystem ? 55 : 180)) {
            [ascRateChangePercent addObject: FBOX(kFirstAscRateChange)];
        } else {
            [ascRateChangePercent addObject: FBOX(kFirstAscRateChange)];
            [ascRateChangePercent addObject: FBOX(kSecondAscRateChange)];
        }
        
        // Call the methods necessary to finish initialization
        [self deepStops];
        [self ascentTime];
        [self buildProfile];
/*     
        [self CNS/OTU];
 */
        
    }
    
    return self;    
}

- (id)init
{
    self = [super init];
    if (self) {
        // Initialization code here.
    }
    
    return self;
}

// Utilitly methods
-(double) aveDepth
{
    return (self.maxDepth + self.gasSwitchDepth) / 2;
}

/****
 * Calculates the change of ascent rate depth.
 * If this dive is in the T2 Range (+180ft/55m) then a second ascent rate change is
 * calcualted.
 *
 * Fills the ascRateChangeDepth array with the depth of when the ascent rate
 * changes occur
 */
-(void) deepStops
{
    int changeDepth = 0;
    
    // If there are more than one depth change elements iterate and calculate
    for (NSNumber *num in ascRateChangePercent) {
        
        double tmpRate = maxDepth * [num floatValue];
        // Round feet/meters up or down as required
        changeDepth =  (tmpRate - (int)tmpRate) >= 0.5 ? ceil(tmpRate) : floor(tmpRate);
        
        int tmp = measurementSystem ? changeDepth % 3 : changeDepth % 10;
        int tmp2 = measurementSystem ? changeDepth / 3 : changeDepth / 10;
        
        if (measurementSystem) {
            changeDepth = tmp >= 1.5 ? (tmp2 + 1) * 3 : tmp2 * 3;
        } else {
            changeDepth = tmp >= 5 ? (tmp2 + 1) * 10 : tmp2 * 10; 
        }
        /**
         * If the gas switch depth is deeper than the last calculated stop,
         * dicard the calculated stop and insert the gasSwitch depth into
         * the ascRateChangeDepth array
         */        
        if (changeDepth <= (int)gasSwitchDepth) {
            [ascRateChangeDepth addObject: [NSNumber numberWithInt:(int)gasSwitchDepth]];
            break;
        }else{
            [ascRateChangeDepth addObject:[NSNumber numberWithInt:changeDepth]];
        }
    }
    if (changeDepth > gasSwitchDepth ) {
        [ascRateChangeDepth addObject:[NSNumber numberWithInt:(int)gasSwitchDepth]];
    }    
}

/****
 * Calcualtes the minimum ascent profile for a T1 and T2 dives.
 *
 * Add a component to calculate Recreational dives.
 */

-(void) ascentTime
{
    
    float accumulatedTime = 0;
    long tmpDepth = maxDepth;
    
    NSArray *ascRateArray = [[NSArray alloc] initWithObjects:[msText objectForKey:@"DeepAscentRate"],
                             [msText objectForKey:@"IntermediateAscentRate"],
                             [msText objectForKey:@"ShallowAscentRate"],
                             nil];
     
    NSEnumerator *e = [ascRateChangeDepth objectEnumerator];
    id element;
    
    int i = 0;
    float timeInterval = 0;
    while ((element = [e nextObject])) {

#undef __TESTING__
#ifdef __TESTING__        
        NSLog(@"The current element of ascRateChangeDepth is %d", [element intValue]);
        NSLog(@"The contents of the ascRateArray is %f", [[ascRateArray objectAtIndex:i] floatValue]);
#endif
        timeInterval = (tmpDepth - [element intValue]) / [[ascRateArray objectAtIndex:i] floatValue];
        
        if ((timeInterval - (int)timeInterval) != 0) {
            timeInterval = (int)timeInterval + 1;
        }
        
        // Save the value
        [ascRateTimeInterval addObject:[NSNumber numberWithFloat:timeInterval]];

        // reset for the next iteration if necessary
        tmpDepth = [element intValue];
        accumulatedTime += timeInterval;
        timeInterval = 0;
        i++;
    }

    accumulatedTime += 4; // 2 minutes for the emergency and 2 minutes for the gas switch;
    
    // Free up the temp array
    [ascRateArray release];
    ascRateArray = nil;

    minGasAscentTime = accumulatedTime;
}

/***
 * This is a pretty packed method. It selects on the whether this is a metric solution. If it is
 * it simply subtracts the setpoint depth from the actual depth and then multiplies by the time per foot/meter
 * calculation once again select by the measurement system. If the product is negative the dive was shallower
 * else it was deeper or positive
 */
-(long) totalDeco
{
    long adjDecoTime = 0;
    long setPoint = measurementSystem ? kRatioDecoMetricSP : kRatioDecoImperialSP;
    float ascRate = measurementSystem ? kRatioDecoAscTimePerMeter : kRatioDecoAscTimePerFoot;
    
    if (maxDepth == setPoint) {
        adjDecoTime = diveTime;
    } else if (maxDepth > setPoint) {
        adjDecoTime = diveTime + ((maxDepth - setPoint) * ascRate);
    } else {
        adjDecoTime = diveTime - (setPoint - maxDepth) * ascRate;
    }
    return adjDecoTime;
}

-(NSArray *) buildProfile 
{
    int decoTime = (int)[self totalDeco];
    // find the nearest interval divided by 5
    int intermediateDeco = (decoTime/2);
    
    // If round up the deco if not a multiple of 5
    if ((intermediateDeco % 5) != 0) {
        intermediateDeco += 5;
    }
    // calc the intermediate interval value
    int intermediateDecoVal = intermediateDeco/5;
    
    // Start with the unadjusted deco
    [diveProfile addObject:[NSString stringWithFormat: kUnadjustedDecoText,
                            [self totalDeco]]];
    [diveProfile addObject: @"The Pragmatic Profile for this dive is:"];

    
    // Add the last stop 
    [diveProfile addObject:[NSString stringWithFormat: kLastStopText,
                            measurementSystem ? 6 : 20,
                            [msText valueForKey:@"Unit"],
                            decoTime / 2]];

    // Add the intermediate stops -- if metric use meters else use feet
    if (measurementSystem) {   
        for (int stopDepth = 9; stopDepth <= 21; stopDepth += 3) {
            [diveProfile addObject:[NSString stringWithFormat: kDecoStopsText,
                                    stopDepth,
                                    [msText valueForKey:@"Unit"],
                                    intermediateDecoVal]];
        }
    } else {
        for (int stopDepth = 30; stopDepth <= 70; stopDepth += 10) {
            [diveProfile addObject:[NSString stringWithFormat: kDecoStopsText,
                                    stopDepth,
                                    [msText valueForKey:@"Unit"],
                                    intermediateDecoVal]];
        }    
    }

    long y = [ascRateChangeDepth count];
    
    for ( NSNumber *num in [ascRateChangeDepth reverseObjectEnumerator]) {
        if ([num intValue] == gasSwitchDepth) {
            continue;
        }
        
        // Add the ascent to the gas switch to the diveProfile
        [diveProfile addObject:[NSString stringWithFormat: kMidAscText,
                                [num intValue],
                                [msText valueForKey:@"Unit"],
                                [[ascRateChangeDepth objectAtIndex: y - 1] intValue],
                                [msText valueForKey:@"Unit"],
                                [[ascRateTimeInterval objectAtIndex: y - 1] floatValue]]];
        y--;
    }
    // Add the first portion of the diveProfile
    [diveProfile addObject:[NSString stringWithFormat: kInitialAscText,
                            [self maxDepth],
                            [msText valueForKey:@"Unit"],
                            [[ascRateChangeDepth objectAtIndex:0] intValue],
                            [msText valueForKey:@"Unit"],
                            [[ascRateTimeInterval objectAtIndex:0] floatValue]]];

    // Add the min gas and the CNS/OTUs
    [diveProfile addObject:[NSString stringWithFormat: kMinGasText,
                            [self minGas],
                            [msText valueForKey:@"VolumeUnit"]]];

    [diveProfile addObject:[NSString stringWithFormat: kCNSText,
                            round([self calcCNS]),
                            [msText valueForKey:@"VolumeUnit"]]];

    return diveProfile;    
}

/***
 * -(int) minGas -- Utility method to calculate Minimum Gas
 *
 * Recieves:    Nothing
 *
 * Returns:     A int value representing the current ATA for the
 *              maxDepth of the dive
 *
 * Revision History: Added 16/04/2100 DBM
 *                   
 *                   Modified 18/04/2011 DBM - Refactored the code
 *                   used to calculate the ATA into a separate
 *                   method. 
 */
-(int) minGas
{
    //    double ata = ca
    
    long time = [self minGasAscentTime];
    time *= 2;                      // Need gas for two divers
    
    return (int)([consumptionRate floatValue] * [self calcATA:maxDepth] * time);
}

/***
 * (long) calcATA : (long) depth -- is a utility method to convert the
 *      depth passed to it to Atomoshperes Absolute
 *
 * Returns: A long value representing the ATA
 *
 * Revision History: Added 18/04/2011 Dan MacKay
 */
- (float) calcATA : (float) theDepth {
    float currentATA;
    if (measurementSystem) {
        // True if metric
        currentATA = (theDepth / 10) + 1;
    } else {
        currentATA = (theDepth / 33) + 1;
    }

    return currentATA;
}
/***
 * -(int) calcCNS: (float) withFO2 : (integer) andDepth; Integer method of calculating CNS
 * 		This method uses the Deep Life Ltd. MatLAB calculation as presented by
 *		Dr. Alex Deas, Dr. Bob Davidov in their paper:
 *		CNS verification_model_080602 released in 2005 and updated in 2008.
 *
 *	Created:	18/04/2011 Dan MacKay
 *
 *	Revision History: 18/04/2011 Dan MacKay
 *					Converted from MattLAB format to Objective C
 */
-(float) calcCNS
{
    /***
     * In order to prepare for this calcultion we need to compute the dive in the
     * following segments:
     *  - descent
     *  - bottom time
     *  - ascent to gas switch
     *  - ascent through gas switch segments
     * Total CNS is calculated by assuming the standard gases for the maxDepth of
     * dive. The calculation is run for every second of the dive, hence all segments
     * need to be converted to seconds.
     */ 

    /*
     * Calculate segment time
     */
    float descentSegInSeconds = (maxDepth / (measurementSystem ? 9 : 30)) * 60;
    float bottomSegInSeconds = diveTime * 60;
    float ascentSegInSeconds = (minGasAscentTime - 4) * 60; // Subtract the time for emergency
    // and gas switch
    float fiftyPercentSegInSeconds = ([self totalDeco]/2) * 60;
    //    float hundredPercentSegInSeconds = ([self totalDeco]/2) * 60;     
    float totalDiveTime = descentSegInSeconds + bottomSegInSeconds + ascentSegInSeconds + fiftyPercentSegInSeconds;
    
    /***
     * Now adjust the segments so they are spaced properly
     */
    bottomSegInSeconds += descentSegInSeconds;
    ascentSegInSeconds += bottomSegInSeconds; // Subtract the time for emergency
    fiftyPercentSegInSeconds = (([self totalDeco]/2) * 60) + ascentSegInSeconds;
    
    float theFO2 = [self selectStandardGas];

    float maxATA = ((float)maxDepth / (measurementSystem ? 10 : 33)) + 1;
    float maxPO2 = theFO2 *  maxATA;
    
    float CNS = 0;
    float PPO2 = 0;
    float dCNS = 0;
    float CNS_TL = 0;

    int t = 0;

    for (t = 1; t < (int)totalDiveTime; t++) { // 1 s time resolution
        
        // PPO2
        if (t <= descentSegInSeconds)	{   // Descent from 0 fsw to 120 fsw at 40 fsw/min
            // PPO2 from 0.32 ATA to 1.484 ATA
            PPO2 = theFO2 + (maxPO2-theFO2)*t/descentSegInSeconds;
        }
        
        if (t > descentSegInSeconds && t <= bottomSegInSeconds) {	// Constant depth at 120 fsw for 22 min
            PPO2 = maxPO2; 			// in ATA
        }
        
        /***
         * This only calculates asent to the first gas switch
         * so it must be evenually modified to incorporate the ascent from
         * the switch on up.
         */
//        if (t > bottomSegInSeconds) { 	// Ascent from depth
//            // PPO2 from 1.484 ATA to 0.32 ATA
//            PPO2 = maxPO2 - (maxPO2-theFO2)*(t-bottomSegInSeconds)/ascentSegInSeconds;
//        }
        if (t > bottomSegInSeconds && t <= fiftyPercentSegInSeconds) { 	// Ascent from depth
            // PPO2 from 1.484 ATA to 0.32 ATA
            PPO2 = maxPO2 - (maxPO2-theFO2)*(t-bottomSegInSeconds)/ascentSegInSeconds;
        }
       
        /***
         * This is for the 21m/70' to 9m/30' ascent
         */
        if (t > fiftyPercentSegInSeconds ) {
            // PPO2 from 1.6 ATA to 0.32 ATA
            //            PPO2 = maxPO2 - (maxPO2-theFO2)*(t-ascentSegInSeconds)/fiftyPercentSegInSeconds;;
            PPO2 = 1.6 - (1.6-theFO2)*(t-ascentSegInSeconds)/fiftyPercentSegInSeconds;;
        }
      
        // Time limit and CNS increment: dCNS
        if (PPO2 < 0.5) {
            dCNS = 0;
        }
        
        if (PPO2 >= 0.5 && PPO2 < 0.6) {
            CNS_TL = 1800 - 1800*PPO2;
            dCNS = 100/(60*CNS_TL);
        }
        
        if (PPO2 >= 0.6 && PPO2 < 0.7) {
            CNS_TL = 1620 - 1500*PPO2;
            dCNS = 100/(60*CNS_TL);
        }
        
        if (PPO2 >= 0.7 && PPO2 < 0.8) {
            CNS_TL = 1410 - 1200*PPO2;
            dCNS = 100/(60*CNS_TL);
        }
        
        if (PPO2 >= 0.8 && PPO2 < 0.9) {
            CNS_TL = 1170 - 900*PPO2;
            dCNS = 100/(60*CNS_TL);
        }
        
        if (PPO2 >= 0.9 && PPO2 < 1.1) {
            CNS_TL = 900 - 600*PPO2;
            dCNS = 100/(60*CNS_TL);
        }
        
        if (PPO2 >= 1.1 && PPO2 < 1.5) {
            CNS_TL = 570 - 300*PPO2;
            dCNS = 100/(60*CNS_TL);
        }
        
        if (PPO2 >= 1.5 && PPO2 < 1.6) {
            CNS_TL = 1245 - 750*PPO2;
            dCNS = 100/(60*CNS_TL);
        }
        // total CNS
        CNS = CNS + dCNS;
        
    }
    
    return CNS;
}

-(NSInteger) calcOTU
{
        return 0;
}
/***
 * -(float) selectStandardGas -- This method simply returns the percent
 *      of O2 in the standard gas based on the maxDepth property.
 *
 *  Input : Nothing
 *  Returns: float
 *
 *	Created:	19/04/2011 Dan MacKay
 *
 *	Revision History:
 */
-(float) selectStandardGas {
    
    /**
     * If an imperial measurement is handed in
     * convert it to metric
     */
    float stdGasO2 = 0;
    
    if (maxDepth <= (measurementSystem ? k32percentMOD_M : k32percentMOD_I)) {
        stdGasO2 = k32percent;
    } else if (maxDepth <= (measurementSystem ? k21percentMOD_M : k21percentMOD_I)) {
        stdGasO2 = k21percent;
    } else if (maxDepth <= (measurementSystem ? k18percentMOD_M : k18percentMOD_I)) {
        stdGasO2 = k18percent;
    } else if (maxDepth <= (measurementSystem ? k15percentMOD_M : k15percentMOD_I)) {
        stdGasO2 = k15percent;
    } else if (maxDepth <= (measurementSystem ? k12percentMOD_M : k12percentMOD_I)) {
        stdGasO2 = k12percent;
    } else {
        stdGasO2 = k10percent;
    }    
    return stdGasO2;
}

- (void)dealloc
{
    [ascRateChangePercent release];
    [ascRateTimeInterval release];
    [ascRateChangeDepth release];
    [diveProfile release];
    [super dealloc];
}

@end
